16장 프로퍼티 어트리뷰트
내부 슬롯과 내부 메서드
내부 슬롯
, 내부 메서드
는 쉽게 말해서 숨겨져 있는 프로퍼티, 메서드라고 생각하면 됨.
JS 엔진에서실제로 동작하지만 개발자가 직접 접근하도록 외부로 공개된 객체의 프로퍼티는 아니다. 그래서 내부 슬롯과 내부 메서드에 직접적으로 접근하거나 호출할 수 없음.
일부에 경우에 간접적인 접근 가능 ( ex, [[Prototype]] )
const o = {};
// 내부 슬롯은 자바스크립트 엔진의 내부 로직이므로 직접 접근할 수 없다.
o.[[Prototype]] // -> Uncaught SyntaxError: Unexpected token '['
// 단, 일부 내부 슬롯과 내부 메서드에 한하여 간접적으로 접근할 수 있는 수단을 제공하기는 한다.
o.__proto__ // -> Object.prototype
프로퍼티 어트리 뷰트와 프로퍼티 디스크립터 객체
프로퍼티 생성하면 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트
를 기본값으로 자동 정의
여기서 프로퍼티 상태란 value, writable, enumeratble, configurable 말함.
각 상태값에 대한 내부 슬롯 [[Value]], [[Writable]], [[Enumeratble]], [[Configurable ]] 이 있는데, 당연히 직접 접근 할 수 없고 Object.getOwnPropertyDescriptor
메서드 사용해서 간접 확인 가능.
const person = {
name: 'Lee'
};
// 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체를 반환한다.
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// {value: "Lee", writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor
( 객체의 참조, 프로퍼티 키(문자열))
프로퍼티 디스크립터
객체 반환함.
프로퍼티 디스크립터
: 프로퍼티 어트리뷰트 정보 객체 형태로 가지고 있음.
Object.getOwnPropertyDescriptors(객체)
는 모든 프로퍼티 디스크립터 객체 반환const person = { name: 'Lee' }; // 프로퍼티 동적 생성 person.age = 20; // 모든 프로퍼티의 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체들을 반환한다. console.log(Object.getOwnPropertyDescriptors(person)); /* { name: {value: "Lee", writable: true, enumerable: true, configurable: true}, age: {value: 20, writable: true, enumerable: true, configurable: true} } */
데이터 프로퍼티와 접근자 프로퍼티
데이터 프로퍼티
: 키와 값으로 구성된 일반적인 프로퍼티
접근자 프로퍼티
: 자체적으로 값 갖지않고 다른 프로퍼티의 값 저장하고 호출하는 접근자 함수
로 구성된 프로퍼티
즉, 접근자 프로퍼티는 값이 아니라, 다른 프로퍼티를 위한 Get, Set 함수로 구성된 일종의 식별자 느낌이랄까.
그래서 프로퍼티 종류마다 가지는 프로퍼티 어트리뷰트
종류가 좀 다름
데이터 프로퍼티
프로퍼티 어트리뷰트 | 프로퍼티 디스크립터 객체의 프로퍼티 |
설명 |
---|---|---|
[[Value]] | value | 프로퍼티 값이라고 보면 됨 |
[[Writable]] | writable | 변경가능 여부, false 일 경우 [[Value]]변경 불가 |
[[Enumerable]] | enumerable | 열거가능 여부, false 일 경우 for문이나 Object.key 메서드에서 열겨 안됨 |
[[Configurable]] | configurable | 재정의 가능 여부, false 일 경우 프로퍼티 삭제, 프로퍼티 어트리뷰트 값 변경 금지. 근데 또, [[Writable]]이 true인 경우에는 [[Value]] 값 변경 가능하고, [[Writable]] flase로 바꾸는건 가능이라나 뭐라나 |
프로퍼티 생성되면 [[Value]]
값은 프로터피 값으로 초기화 되고, 나머지 어트리뷰트는 true로 초기화 된다. ( 동적 추가도 마찬가지 )
접근자 프로퍼티
프로퍼티 어트리뷰트 | 프로퍼티 디스크립터 객체의 프로퍼티 |
설명 |
---|---|---|
[[Get]] | get | getter 함수 호출 |
[[Set]] | set | setter 함수 호출 |
[[Enumerable]] | enumerable | 데이터 프로퍼티와 동일 |
[[Configurable]] | configurable | 데이터 프로퍼티와 동일 |
const person = {
// 데이터 프로퍼티
firstName: 'Ungmo',
lastName: 'Lee',
// fullName은 접근자 함수로 구성된 접근자 프로퍼티다.
// getter 함수
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
// setter 함수
set fullName(name) {
// 배열 디스트럭처링 할당: "31.1 배열 디스트럭처링 할당" 참고
[this.firstName, this.lastName] = name.split(' ');
}
};
// 데이터 프로퍼티를 통한 프로퍼티 값의 참조.
console.log(person.firstName + ' ' + person.lastName); // Ungmo Lee
// 접근자 프로퍼티를 통한 프로퍼티 값의 저장
person.fullName = 'Heegun Lee';
console.log(person); // {firstName: "Heegun", lastName: "Lee"}
// 접근자 프로퍼티를 통한 프로퍼티 값의 참조
console.log(person.fullName); // Heegun Lee
// firstName은 데이터 프로퍼티다.
// 데이터 프로퍼티는 [[Value]], [[Writable]], [[Enumerable]], [[Configurable]] 프로퍼티 어트리뷰트를 갖는다.
let descriptor = Object.getOwnPropertyDescriptor(person, 'firstName');
console.log(descriptor);
// {value: "Heegun", writable: true, enumerable: true, configurable: true}
// fullName은 접근자 프로퍼티다.
// 접근자 프로퍼티는 [[Get]], [[Set]], [[Enumerable]], [[Configurable]] 프로퍼티 어트리뷰트를 갖는다.
descriptor = Object.getOwnPropertyDescriptor(person, 'fullName');
console.log(descriptor);
// {get: ƒ, set: ƒ, enumerable: true, configurable: true}
set
, get
으로 접근자 프로퍼티의 접근자 함수 선언
접근자 프로퍼티 이용할 때, 함수처럼 인자를 전달하는게 아니라 변수처럼 사용함.
프로퍼티 정의
새로운 프로퍼티 추가하면서 프로퍼티 어트리뷰트
를 명시적으로 정의하거나 기존 프로퍼티의 어트리뷰트 재정의 가능함.
Object.defineProperty
(객체의 참조, 프로퍼티 키, 프로퍼티 디스크립터 객체 ) 이용const person = {}; // 데이터 프로퍼티 정의 Object.defineProperty(person, 'firstName', { value: 'Ungmo', writable: true, enumerable: true, configurable: true }); Object.defineProperty(person, 'lastName', { value: 'Lee' }); let descriptor = Object.getOwnPropertyDescriptor(person, 'firstName'); console.log('firstName', descriptor); // firstName {value: "Ungmo", writable: true, enumerable: true, configurable: true} // 디스크립터 객체의 프로퍼티를 누락시키면 undefined, false가 기본값이다. descriptor = Object.getOwnPropertyDescriptor(person, 'lastName'); console.log('lastName', descriptor); // lastName {value: "Lee", writable: false, enumerable: false, configurable: false} // [[Enumerable]]의 값이 false인 경우 // 해당 프로퍼티는 for...in 문이나 Object.keys 등으로 열거할 수 없다. // lastName 프로퍼티는 [[Enumerable]]의 값이 false이므로 열거되지 않는다. console.log(Object.keys(person)); // ["firstName"] // [[Writable]]의 값이 false인 경우 해당 프로퍼티의 [[Value]]의 값을 변경할 수 없다. // lastName 프로퍼티는 [[Writable]]의 값이 false이므로 값을 변경할 수 없다. // 이때 값을 변경하면 에러는 발생하지 않고 무시된다. person.lastName = 'Kim'; // [[Configurable]]의 값이 false인 경우 해당 프로퍼티를 삭제할 수 없다. // lastName 프로퍼티는 [[Configurable]]의 값이 false이므로 삭제할 수 없다. // 이때 프로퍼티를 삭제하면 에러는 발생하지 않고 무시된다. delete person.lastName; // [[Configurable]]의 값이 false인 경우 해당 프로퍼티를 재정의할 수 없다. // Object.defineProperty(person, 'lastName', { enumerable: true }); // Uncaught TypeError: Cannot redefine property: lastName descriptor = Object.getOwnPropertyDescriptor(person, 'lastName'); console.log('lastName', descriptor); // lastName {value: "Lee", writable: false, enumerable: false, configurable: false} // 접근자 프로퍼티 정의 Object.defineProperty(person, 'fullName', { // getter 함수 get() { return `${this.firstName} ${this.lastName}`; }, // setter 함수 set(name) { [this.firstName, this.lastName] = name.split(' '); }, enumerable: true, configurable: true }); descriptor = Object.getOwnPropertyDescriptor(person, 'fullName'); console.log('fullName', descriptor); // fullName {get: ƒ, set: ƒ, enumerable: true, configurable: true} person.fullName = 'Heegun Lee'; console.log(person); // {firstName: "Heegun", lastName: "Lee"}
Object.defineProperties
이용해서 한번에 여러개의 프로퍼티도 정의 가능
const person={
name:"chanho"
}
console.log(Object.getOwnPropertyDescriptor(person,'name'));
//{value: 'chanho', writable: true, enumerable: true, configurable: true}
//객체 직접 수정 시
Object.getOwnPropertyDescriptor(person,'name').writable=false
console.log(Object.getOwnPropertyDescriptor(person,'name'));
//{value: 'chanho', writable: true, enumerable: true, configurable: true}
프로퍼티 어트리뷰트들을 객체의 형태로 존재하는게 아니라 하나하나의 변수 형태로 존재하고, 디스크립터 객체는 그 어트리뷰트들을 모아서 객체형태로 반환한 형태 일 듯?
그래서 디스크립터 객체 바꿔도 실제로 어트리뷰트들을 바뀌는 않는 것 같음. #뇌피셜
객체 변경 방지
객체는 변경 가능한 값이여서 재할당 없이도 값 변경이 가능함
⇒ 객체의 변경을 방지하는 다양한 메서드들이 있다고 한다.
Object.preventExtensions
: 객체 확장 금지
Object.seal
: 객체 밀봉 (프로퍼티 삭제, 어트리뷰트 재정의 금지)
Object.freeze
: 객체 동결 ( 밀봉 + 프로퍼티 값 수정 불가 )
여기서 어트리뷰트 재정의라는 표현을 씀. 즉, 어트리뷰트들이 객체의 형태가 아닌 변수. 즉 원시값 변수형태? 여서 수정 대신 재정의라는 표현을 쓴 듯. 고로 위의 나의 뇌피셜에 대한 검증
(원시갑과 객체의 재할당에 대한 차이는 11장 원시 값과 객체의 비교 참조)
객체 불변
Object.freeze
사용해도 중첩객체 까지는 동결 시킬 수 업음.
⇒ 중첩 객체도 동결해서 불변 객체
만들려고 하면 재귀적으로 얼려주면 됨.
function deepFreeze(target) {
// 객체가 아니거나 동결된 객체는 무시하고 객체이고 동결되지 않은 객체만 동결한다.
if (target && typeof target === 'object' && !Object.isFrozen(target)) {
Object.freeze(target);
/*
모든 프로퍼티를 순회하며 재귀적으로 동결한다.
Object.keys 메서드는 객체 자신의 열거 가능한 프로퍼티 키를 배열로 반환한다.
("19.15.2. Object.keys/values/entries 메서드" 참고)
forEach 메서드는 배열을 순회하며 배열의 각 요소에 대하여 콜백 함수를 실행한다.
("27.9.2. Array.prototype.forEach" 참고)
*/
Object.keys(target).forEach(key => deepFreeze(target[key]));
}
return target;
}
const person = {
name: 'Lee',
address: { city: 'Seoul' }
};
// 깊은 객체 동결
deepFreeze(person);
console.log(Object.isFrozen(person)); // true
// 중첩 객체까지 동결한다.
console.log(Object.isFrozen(person.address)); // true
person.address.city = 'Busan';
console.log(person); // {name: "Lee", address: {city: "Seoul"}}